package rsj.proxy.strategy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import rsj.annotation.DynamicSelect;
import rsj.context.ApplicationContextUtil;
import rsj.proxy.PreExecuteStatementProcessor;
import rsj.proxy.parse.SqlParse;
import rsj.proxy.parse.SqlSource;

import java.beans.Introspector;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 执行动态sql
 *
 * @author yuanqinglong
 * @since 2021/5/8 10:13
 */
@Component
public class DynamicSelectAnnotationStrategy implements IExecuteStrategy, ApplicationContextAware {
    private static final Logger logger = LoggerFactory.getLogger(DynamicSelectAnnotationStrategy.class);

    /**
     * 缓存创建的动态SQL声明类对象、缓存单例节省内存
     */
    private final Map<Class<?>, Object> dynamicSqlClassCache = new HashMap<>(16);

    private Set<String> beanNames = null;


    @Override
    public Class<?> getSupportAnnotationType() {
        return DynamicSelect.class;
    }


    @Override
    public Object executeStrategy(Connection connection, Method method, Object[] args) throws Exception {
        DynamicSelect dynamicSelect = method.getAnnotation(DynamicSelect.class);
        Class<?> type = dynamicSelect.type();
        Method declaredMethod = type.getDeclaredMethod(dynamicSelect.method(), method.getParameterTypes());
        String sql = (String) declaredMethod.invoke(this.getDynamicSqlClassCache(type), args);
        SqlSource sqlSource = SqlParse.createSqlSource(sql, method);
        Object[] params = this.paramResolve(sqlSource, method, args);
        PreExecuteStatementProcessor executeProcessor = this.getExecuteProcessor(method);
        //logger.info("执行SQL: \n\n{}\n", this.printSql(sqlSource, params));
        logger.info("执行SQL: \n\n{} \n参数:{}", sqlSource.getFinalSql(),params);
        return executeProcessor.execute(connection, sqlSource.getFinalSql(), params);
    }


    /**
     * 获取动态sql声明类、保证单例
     *
     * @param type 动态sql声明类类型
     * @return 动态sql声明类
     * @throws Exception 异常
     */
    private Object getDynamicSqlClassCache(Class<?> type) throws Exception {
        Object cache = dynamicSqlClassCache.get(type);
        if (Objects.isNull(cache)) {
            synchronized (dynamicSqlClassCache) {
                cache = this.getFromSpring(type);
                if (Objects.nonNull(cache)) {
                    return cache;
                }
                cache = dynamicSqlClassCache.get(type);
                if (Objects.isNull(cache)) {
                    cache = type.newInstance();
                    dynamicSqlClassCache.putIfAbsent(type, cache);
                }
            }
        }
        return cache;
    }


    /**
     * 从Spring容器获取bean
     *
     * @param type 类型
     * @return {@link Object}
     */
    private Object getFromSpring(Class<?> type) {
        if (type.isAnnotationPresent(Component.class)) {
            Component component = type.getAnnotation(Component.class);
            String beanName = StringUtils.isEmpty(component.value()) ? this.getBeanDefaultName(type) : component.value();
            if (beanNames.contains(beanName)) {
                Object bean = ApplicationContextUtil.getBean(beanName);
                dynamicSqlClassCache.putIfAbsent(type, bean);
                return bean;
            }
        }
        return null;
    }


    /**
     * 获取bean默认的名称
     *
     * @param type 类型
     * @return {@link String}
     */
    private String getBeanDefaultName(Class<?> type) {
        String shortClassName = ClassUtils.getShortName(type.getName());
        return Introspector.decapitalize(shortClassName);
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        beanNames = Stream.of(applicationContext.getBeanDefinitionNames()).collect(Collectors.toSet());
    }
}
